package com.k8.common.aop;

import com.k8.common.aop.javassist.JavassistAopProxy;
import com.k8.common.aop.internal.advisor.Advisor;
import com.k8.common.util.AopUtil;
import com.k8.rpc.url.URL;
import javassist.*;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class JavassistProxyFactory implements ProxyFactory {
    private final NamingPolicy namingPolicy = new NamingPolicy();
    private final String INTERCEPTOR = "k8$interceptor";
    private final String METHOD_ARRAY = "methodArray";

    @Override
    public Object build(Object target, List<Advisor> cacheAdvisors) {
        boolean proxyNecessary = AdvisedSupport.isProxyNecessary(target, cacheAdvisors);
        if (!proxyNecessary) {
            AdvisedSupport.cacheCandidates.remove();
            return target;
        }
        AdvisedSupport advisedSupport = new AdvisedSupport(target);
        JavassistAopProxy javassistAopProxy = new JavassistAopProxy(advisedSupport, namingPolicy);
        return javassistAopProxy.getProxy();
    }

    @Override
    public Object build(URL url, Object target, List<Advisor> cacheAdvisors) {
        return build(target, cacheAdvisors);
    }

    @Override
    public <T> T createInterfaceProxy(Class<T> interfaceClazz, InterfaceProxyMethodInterceptor interceptor) {
        return createInterfaceProxy(interfaceClazz, determineClassLoader(interfaceClazz.getClassLoader()), interceptor);
    }

    @Override
    public <T> T createInterfaceProxy(Class<T> interfaceClazz, ClassLoader classLoader, InterfaceProxyMethodInterceptor interceptor) {
        if (!interfaceClazz.isInterface())
            throw new RuntimeException("Class is not a interface , class: " + interfaceClazz.getName());
        ClassPool pool = ClassPool.getDefault();
        pool.appendClassPath(new ClassClassPath(classLoader.getClass()));
        try {
            CtClass proxy = pool.makeClass(namingPolicy.getProxyName(interfaceClazz));
            Method[] declaredMethods = interfaceClazz.getDeclaredMethods();
            CtClass interfaceCtClazz = pool.get(interfaceClazz.getName());
            proxy.addInterface(interfaceCtClazz);
            CtClass methodArrayCtClass = pool.get(Method[].class.getName());
            proxy.addField(new CtField(methodArrayCtClass, METHOD_ARRAY, proxy));
            CtClass interceptorCtClass = pool.get(InterfaceProxyMethodInterceptor.class.getName());
            proxy.addField(new CtField(interceptorCtClass, INTERCEPTOR, proxy));
            int index = 0;
            for (Method method : declaredMethods) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                CtClass[] ctParameters = new CtClass[parameterTypes.length];
                for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> clazz = parameterTypes[i];
                    ctParameters[i] = AopUtil.getCtClassForPrimitive(clazz, pool);
                }
                Class<?> returnType = method.getReturnType();
                CtClass ctReturnType = pool.get(returnType.getName());
                String body;
                CtMethod ctMethod = new CtMethod(ctReturnType, method.getName(), ctParameters, proxy);
                if (returnType == Void.TYPE) {
                    body = String.format(
                            "{this.%s.process(this, %s[%d], $args);}",
                            INTERCEPTOR, METHOD_ARRAY, index
                    );
                } else {
                    body = String.format(
                            "{ " +
                                    "Object result = this.%s.process(this, %s[%d], $args);\n" +
                                    "if (result == null) {\n" +
                                    "    return null;\n" +
                                    "}\n" +
                                    "return (%s) result;\n" +
                                    "}",
                            INTERCEPTOR, METHOD_ARRAY, index,
                            returnType.getName()
                    );
                }
                ctMethod.setBody(body);
                proxy.addMethod(ctMethod);
                index++;
            }

            Class<?> proxyClass = proxy.toClass();
            T instance = (T) proxyClass.newInstance();
            Field methodAF = proxyClass.getDeclaredField(METHOD_ARRAY);
            methodAF.setAccessible(true);
            methodAF.set(instance, declaredMethods);
            Field interceptorF = proxyClass.getDeclaredField(INTERCEPTOR);
            interceptorF.setAccessible(true);
            interceptorF.set(instance, interceptor);
            return instance;
        } catch (NotFoundException | CannotCompileException | NoSuchFieldException | InstantiationException |
                 IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }


    private ClassLoader determineClassLoader(ClassLoader classLoader) {
        if (classLoader == null) {
            return this.getClass().getClassLoader();
        } else {
            if (classLoader.getParent() == null) {
                ClassLoader aopClassLoader = this.getClass().getClassLoader();
                for (ClassLoader aopParent = aopClassLoader.getParent(); aopParent != null; aopParent = aopParent.getParent()) {
                    if (classLoader == aopParent) {
                        return aopClassLoader;
                    }
                }
            }
            return classLoader;
        }
    }

    public static class NamingPolicy {
        ConcurrentHashMap<Class<?>, AtomicInteger> classProxyCounter = new ConcurrentHashMap<>();

        public String getProxyName(Class<?> clazz) {
            AtomicInteger counter = classProxyCounter.get(clazz);
            if (counter == null) {
                counter = classProxyCounter.computeIfAbsent(clazz, (key) -> {
                    return new AtomicInteger(0);
                });
            }
            return clazz.getName() + "$" + counter.getAndIncrement();
        }
    }
}
